package com.wolfking.converter.spring;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.wolfking.converter.enums.ConverterType;
import com.wolfking.converter.service.JdbcTemplateService;
import com.wolfking.converter.transform.FieldTransform;
import lombok.extern.slf4j.Slf4j;
import org.reflections.ReflectionUtils;
import org.springframework.util.CollectionUtils;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 经过隔壁老王处理的JDK动态代理的实现
 * converter interface的动态代理
 * Object的方法调用this的方法
 *
 * @author 赵伟伟(wolfking)
 * created on 2019-09-29 13:33
 * Email is zhaoweiwei@youxin.com
 * Copyright is 友信金服
 */
@Slf4j
class JdkConverterProxy implements InvocationHandler {

    /**
     * spi的代理接口
     */
    private final Class<?> converterInterface;

    /**
     * jdbc的扩展增强
     */
    private final JdbcTemplateService jdbcTemplateService;

    /**
     * 方法到SQL的映射
     */
    private final Map<Method, String> methodSqlMap = Maps.newHashMap();

    /**
     * 方法到JSON和FIELD转换类型的映射
     */
    private final Map<Method, ConverterType> methodConverterTypeMap = Maps.newHashMap();

    /**
     * 方法到字段转换类的映射
     */
    private final Map<Method, Map<String, FieldTransform<?>>> fieldTransformMap = Maps.newHashMap();

    /**
     * sql中问号绑定还是  冒号参数绑定
     */
    private final Map<Method, Boolean> methodQuestionMark = Maps.newHashMap();

    /**
     * sql中冒号参数绑定的名字
     */
    private final Map<Method, List<String>> methodParamName = Maps.newHashMap();

    private final Map<Method, String> dataSourceMap = Maps.newHashMap();

    @SuppressWarnings("all")
    JdkConverterProxy(Class<?> converterInterface, JdbcTemplateService jdbcTemplateService) {
        this.jdbcTemplateService = jdbcTemplateService;
        this.converterInterface = converterInterface;
        Set<Method> methods = ReflectionUtils.getAllMethods(converterInterface);
        for (Method method : methods) {
            if (!method.isDefault() && !method.getDeclaringClass().equals(Object.class)) {
                ConverterMethod annotation = method.getAnnotation(ConverterMethod.class);
                if (annotation == null) {
                    String message = "parse %s->%s ,no ConverterMethod annotation";
                    throw new RuntimeException(String.format(message, converterInterface.getName(), method.getName()));
                }
                /*解析SQL*/
                methodSqlMap.put(method, assemblySql(method));
                dataSourceMap.put(method, annotation.dataSource());
                methodConverterTypeMap.put(method, annotation.converterType());
                /*解析转换器*/
                Map<String, FieldTransform<?>> transformHashMap = Maps.newHashMap();
                CustomConverters customConverters = method.getAnnotation(CustomConverters.class);
                CustomConverter customConverter = method.getAnnotation(CustomConverter.class);
                CustomConverter[] value = new CustomConverter[]{};
                if (customConverters != null) {
                    value = customConverters.value();
                } else if (customConverter != null) {
                    value = new CustomConverter[]{customConverter};
                }
                for (CustomConverter converter : value) {
                    Class<? extends FieldTransform<?>> transform = converter.transform();
                    String arg = converter.constructArg();
                    try {
                        FieldTransform<?> fieldTransform = arg.trim().length() == 0 ?
                                transform.newInstance() : transform.getDeclaredConstructor(String.class).newInstance(arg);
                        transformHashMap.put(converter.name(), fieldTransform);
                    } catch (Exception e) {
                        String message = "parse %s->%s , CustomConverter %s error";
                        throw new RuntimeException(String.format(message, converterInterface.getName(),
                                method.getName(), converter.name()));
                    }
                }
                fieldTransformMap.put(method, transformHashMap);
            } else {
                log.debug("current method {} skip", method);
            }
        }
    }

    /**
     * 动态代理方法调用
     *
     * @param proxy  代理类
     * @param method 当前方法
     * @param args   参数
     * @return 处理结果
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /*object的方法*/
        if (method.isDefault()) {
            /*default的方法*/
            Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
            constructor.setAccessible(true);
            Class<?> declaringClass = method.getDeclaringClass();
            int allModes = MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE;
            return constructor.newInstance(declaringClass, allModes)
                    .unreflectSpecial(method, declaringClass)
                    .bindTo(proxy)
                    .invokeWithArguments(args);
        } else if (method.getDeclaringClass().equals(Object.class)) {
            return method.invoke(this, args);
        } else {
            /*代理的方法*/
            return invokeMethod(method, args);
        }
    }

    /**
     * 获取代理对象呢
     *
     * @return Spi的动态代理
     */
    Object getConverterProxy() {
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{converterInterface}, this);
    }

    private Object invokeMethod(Method method, Object[] args) {
        String sql = methodSqlMap.get(method);
        if (List.class.isAssignableFrom(method.getReturnType()) || Collection.class.isAssignableFrom(method.getReturnType())) {
            ParameterizedType genericReturnType = (ParameterizedType) method.getGenericReturnType();
            Class<?> clazz = (Class<?>) genericReturnType.getActualTypeArguments()[0];
            if (methodQuestionMark.get(method)) {
                return jdbcTemplateService.queryList(dataSourceMap.getOrDefault(method, ""), sql, clazz,
                        fieldTransformMap.get(method), methodConverterTypeMap.get(method), args);
            } else {
                Map<String, Object> paramMap = assemblyMapParams(method, args);
                return jdbcTemplateService.queryList(dataSourceMap.getOrDefault(method, ""), sql, clazz,
                        fieldTransformMap.get(method), methodConverterTypeMap.get(method), paramMap);
            }
        } else {
            if (methodQuestionMark.get(method)) {
                return jdbcTemplateService.queryOne(dataSourceMap.getOrDefault(method, ""), sql, method.getReturnType(),
                        fieldTransformMap.get(method), methodConverterTypeMap.get(method), args);
            } else {
                Map<String, Object> paramMap = assemblyMapParams(method, args);
                return jdbcTemplateService.queryOne(dataSourceMap.getOrDefault(method, ""), sql, method.getReturnType(),
                        fieldTransformMap.get(method), methodConverterTypeMap.get(method), paramMap);
            }
        }
    }

    private Map<String, Object> assemblyMapParams(Method method, Object[] args) {
        Map<String, Object> paramMap = Maps.newHashMap();
        List<String> paramNames = methodParamName.get(method);
        for (int i = 0; i < args.length; i++) {
            paramMap.put(paramNames.get(i), args[i]);
        }
        return paramMap;
    }

    private String assemblySql(Method method) {
        ConverterMethod converterMethod = method.getAnnotation(ConverterMethod.class);
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        List<String> params = Lists.newArrayList();
        for (Annotation[] annotations : parameterAnnotations) {
            for (Annotation annotation : annotations) {
                if (annotation instanceof SqlParam) {
                    String value = ((SqlParam) annotation).value().trim();
                    if (params.contains(String.format("#{%s}", value))) {
                        String message = "parse %s->%s , parameter[%s] exists multiple error";
                        throw new RuntimeException(String.format(message, converterInterface.getName(),
                                method.getName(), value));
                    }
                    params.add(value);
                }
            }
        }
        if (CollectionUtils.isEmpty(params)) {
            methodQuestionMark.put(method, true);

        } else {
            methodQuestionMark.put(method, false);
            methodParamName.put(method, params);
            if (params.size() != method.getParameterCount()) {
                String message = "parse %s->%s , SqlParam lost error";
                throw new RuntimeException(String.format(message, converterInterface.getName(), method.getName()));
            }
        }
        return converterMethod.sql();
    }
}
